#!/usr/bin/perl -w
# nexys2prog - program a Digilent Nexys 2 FPGA board over USB
# Copyright (c) 2009, Andrew Ross <andy@plausible.org>
# Please see below for license terms (GPL).
# http://ixo-jtag.sourceforge.net/
#
# Usage: nexys2prog [-v|--verbose] <Bitstream File>
#
# This script automatically finds an attached Nexys 2 board and loads
# it with the specified Xilinx bitstream with a minimum of fuss,
# configuration, and external dependencies. Specifically, the
# hacked/patched firmware for the Cypress FX2 chip is provided.
# The user only needs user-level software installed and a single local
# configuration change (optional, for the USB device files -- the lazy
# can just run the script as root). Please see the file README.txt
# for additional documentation.
#
# Prerequisites:
#
# 1. A working Xilinx ISE installation. This was tested against 10.1 and
#    11.1, but I believe older versions share the same iMPACT syntax and
#    BSDL file locations.
#
# 2. The "fxload" utility is required to reprogram the board's FX2 USB
#    chip, available in Debian and Ubuntu via "apt-get install
#    fxload". Note that fxload requires write access to the raw USB
#    device files under /dev/bus/usb, and that these are by default
#    read-only on Ubuntu Intrepid. You can either run the script as
#    root, or else set the files to be owned by the "plugdev" group by
#    adding the GROUP field to this line in
#    /etc/udev/rules.d/40-basic-permissions.rules:
#    SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0664", GROUP="plugdev"
#
# 3. UrJTAG, available from http://urjtag.org. UrJTAG is an active
#    fork of the moribund openwince-jtag project. Note that
#    openwince-jtag is still an available Debian/Ubuntu package, that
#    it shares the same "jtag" binary name and most of its syntax, and
#    that IT DOES NOT WORK with the firmware in this script. You need
#    to install UrJTAG. Also note that you will need libftdi
#    installed for the protocol handler, again available on
#    Debian/Ubuntu via "apt-get install libftdi1". UrJTAG will build
#    without this, but you won't be able to program the Nexys 2
#    without libftdi.
#
# Note that this script comes with a binary firmware blob built from
# free software sources. See the file README.txt for more information.
#
# TODO:
# + Figure out the JTAG interface for the boot PROM, so it can be
#   flashed with the bitstream instead of (or in addition to) doing a
#   direct load to the FPGA.
# + Pull down and parse the JTAG chain from the device to verify that
#   it's actually a Nexys 2 and not another device sharing the same
#   firmware family and bus ID. Kolja's firmware runs on other
#   devices too...
# + Extend the script to recognize arbitrary JTAG chains and find the
#   appropriate part number automatically. So you'd just specify a
#   .bit file (which contains the FPGA type) and it would crawl the
#   JTAG bus looking for a matching FPGA to program.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program as the file COPYING.txt; if not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.
#
use warnings;
use strict;
use POSIX qw(isatty);
use Time::HiRes qw(usleep);
use FindBin;

my $USBID_KOLJA = "16c0:06ad";
my $USBID_DIGILENT = "1443:0005";
my $XILINX;
my $TMPID = sprintf("/tmp/nexys2prog-%8.8X-%4.4X", time(), $$);
my $FIRMWARE_FILE = $FindBin::RealBin."/usb_jtag.hex";

my $verbose = 0;
my $filebase;
my $bit;

# Parse the command line
while(@ARGV) {
    my $arg = shift;
    if($arg eq "-v" or $arg eq "--verbose") { $verbose = 1; }
    elsif(!defined $bit and -f $arg and -R $arg) {
	$bit = $arg;
	$filebase = $bit;
	$filebase =~ s/\.bit$//i;
    } else { usage(); }
}
usage() if !defined $bit;

# Find the tools
check_tools();
find_xilinx();

# If needed, convert the .bit file to .svf using Xilinx's iMPACT tool.
update_svf();

# Locate and configure the board's USB interface
find_board();

# Do it
play_svf();

########################################################################

sub usage { die "Usage:\n  $0 [-v|--verbose] <Bitstream File>\n"; }
sub vlog { print join("", @_) if $verbose; }

# Idiot-proofing, to help mitigate the Rube Goldbergisms inherent in
# this process.
sub check_tools {
    system("which fxload >/dev/null") == 0 or die
	("Cannot find fxload executable.\n".
	 "Try:  \"apt-get install fxload\" ?\n");
    system("which impact >/dev/null") == 0 or die
	("Cannot find iMPACT executable.  The Xilinx ISE development kit\n".
	 "must be installed and its settings32.sh script sourced in the\n".
	 "current shell.\n");
    system("which jtag >/dev/null") == 0 or die
	("Cannot find jtag executable.  The UrJTAG tools (http://urjtag.org)\n".
	 "must be built, installed, and on the PATH.  You will also need to\n".
	 "install libftdi as a prerequisite: \"apt-get install libftdi-dev\" if\n".
	 "needed.\n");
    system("jtag --version | grep UrJTAG >/dev/null") == 0 or die
	("Installed jtag executable is the wrong version.  You probably have the\n".
	 "openwince-jtag tools installed.  The Nexys2 loader requires UrJTAG\n".
	 "(http://urjtag.org) instead, which is a fork that, sadly, shares the\n".
	 "same command names.  Uninstall the existing software, or (carefully!)\n".
	 "install UrJTAG such that it lives before the old tool on your PATH.\n");
}

sub find_board {
    my $nx = find_nexys2();
    die "Cannot find a Nexys 2 board on the USB bus.  Plug it in?\n"
	if(!defined $nx);
    my ($bus, $dev, $id) = @$nx;
    if($id ne $USBID_KOLJA) {
	$dev = blasterize($bus, $dev);
    } else {
	vlog("Found already-configured board on bus $bus dev $dev\n");
    }
}

sub update_svf {
    my $svf = "$filebase.svf";
    if((!-f $svf) or (stat($svf))[9] <= (stat($bit))[9]) {
	vlog("Generating SVF file...\n");
	my $impactrc = "$TMPID.impact";
	open RC, ">$impactrc" or die "can't write to $TMPID.impact";
	print RC "setMode -bs\n";
	print RC "setCable -port svf -file $svf\n";
	print RC "addDevice -p 1 -file $XILINX/xcf/data/xcf04s.bsd\n";
	print RC "addDevice -p 2 -file $bit\n";
	print RC "program -p 2\n";
	print RC "closeCable\n";
	print RC "quit\n";
	close RC;
	if($verbose) { system("sed 's/^/  /' $impactrc"); }
	run("impact -batch $impactrc");
	die "iMPACT failed to generate $svf" if ! -f $svf;
    } else {
	vlog("$svf is current, not regenerating.\n");
    }
}

sub find_xilinx {
    my $ise = `which impact`;
    $ise = qx(cd `dirname $ise`; /bin/pwd);
    chomp $ise;
    die "Cannot find Xilinx ISE installation directory.\n"
       if $ise !~ /ISE\/bin\/lin(64)?$/;
    $ise =~ s/\/bin\/lin(64)?$//;
    die "Cannot understand Xilinx ISE installation tree.\n"
	if !-d "$ise/spartan3e/data" or !-f "$ise/xcf/data/xcf04s.bsd";
    $XILINX = $ise;
    vlog("Located Xilinx ISE installation in $XILINX.\n");
}

# Run an external tool, emitting the output only if it fails. iMPACT
# and UrJTAG are annoyingly verbose...
sub run {
    my $cmd = shift;
    print($cmd, "\n") if $verbose;
    open CMD, "$cmd 2>&1|" or die;
    # FIXME: if the subprocess crashes (as jtag does without a cable),
    # we get incomplete output...
    my $out = join "", <CMD>;
    close CMD;
    die "Command failure:\n  $cmd\n\n$out" if $?;
}

sub find_nexys2 {
    my ($bus, $dev, $id);
    open LSUSB, "lsusb|" or die "Cannot run lsusb";
    while(<LSUSB>) {
	next if ! /Bus (\d+) Device (\d+): ID ([0-9a-f]{4}:[0-9a-f]{4})/;
	next if !($3 eq $USBID_DIGILENT or $3 eq $USBID_KOLJA);
	($bus, $dev, $id) = ($1, $2, $3);
    }
    close LSUSB;
    if(defined $bus) {
	vlog("Found USB device $id on bus $bus device $dev\n");
	return [$bus, $dev, $id];
    }
    return undef;
}

# Reprogram a connected FX2 device with Kolja Waschk's usb_jtag
# firmware, patched to support the Nexys 2 pin assignments and FPGA
# bitstream sizes as per Morgan Delahaye's instructions at
# http://m-del.net/info.html. Uses the fxload tool from the
# linux-hotplug project. The end result is an interface compatible
# with the FTDI-based Altera USBBlaster product.
sub blasterize {
    my ($bus, $dev) = @_;
    my $usbfile = "/dev/bus/usb/$bus/$dev";
    if(!-w $usbfile) {
	die ("Cannot write to $usbfile.\n\n" .
	     "Either run this tool as root or modify your udev settings to\n" .
	     "allow write access to USB device files.\n");
    }
    my $firmware = $FIRMWARE_FILE;
    vlog("Loading 8051 firmware into board...\n");
    run("fxload -t fx2 -D $usbfile -I $firmware");
    my $nx;
    # Wait for it to reboot, renumerate and appear on the bus.
    for(my $i=0; $i<20; $i++) {
	usleep 10000;
	last if defined($nx = find_nexys2()) and $nx->[2] eq $USBID_KOLJA;
    }
    if(!defined $nx or $nx->[2] ne $USBID_KOLJA) {
	die ("Reprogrammed FX2 device not found on USB bus.\n",
	     "fxload failure? device unplugged? ... !?\n");
    }
    return $nx->[1];
}

sub play_svf {
    open JTAG, ">$TMPID.jtag" or die "cannot write to $TMPID.jtag";
    # Just the BSDL directories needed for the 2 chips on the Nexys2...
    print JTAG "bsdl path $XILINX/spartan3e/data;$XILINX/xcf/data\n";
    print JTAG "cable usbblaster\n";
    print JTAG "detect\n";
    print JTAG "part 1\n";
    print JTAG "svf $filebase.svf\n";
    print JTAG "quit\n";
    close JTAG;
    run("jtag $TMPID.jtag");
}

